还没有笔记
选中页面文字后点击「高亮」按钮添加
gcc -c helloworld.c -o helloworld.o
```
gcc 执行 GNU C 编译器。
-c 表示只编译以生成目标文件,而不链接到最终的可执行文件。
$-\circ$ 后跟文件名。标志 -o 告诉编译器输出文件的名称。在本例中,我们希望目标文件 helloworld.o。如果省略 -o helloworld.o,它仍然会创建 helloworld.o,因为目标文件的默认行为是使用与源文件相同的名称,但将扩展名从 .C 更改为 .o。
```
objdump -d helloworld.o
```
objdump 显示一个或多个目标文件的信息。
此信息主要对从事编译工具的程序员有用,而不是只希望程序编译和工作的程序员。
尽管我们可以用单个 gcc helloworld.c 命令生成 helloworld.c 的二进制文件,但有 4 个阶段在一个黑盒中发生。
gcc 是主程序,它调用其他可执行文件独立完成任务。
```
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (l)));
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1)));
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
int main() {
printf("%s\n", "Hello, world!");
return 0;
}
```
```
.file "helloworld.c"
.text
.section .rodata
.LCO :
.string "Hello, world!"
.text
.globl main
.type main, @function
main:
.LFB0:
.file "helloworld.c"
.text
.section .rodata
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LCO(%rip), %rax
movq %rax, %rdi
call puts@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 14.2.0-4ubuntu2) 14.2.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
```
将汇编代码转换为机器码

链接目标文件并输出最终的可执行文件。

| $\begin{aligned} & \# \text { ifndef } \quad \text { _ADD_H_ } \\ & \text { \#define _ADD_H_ } \end{aligned}$
\#endif | \#include "add.h" add.c
int add(int x, int y) \{ return x + y;
\} | \#include
\#include
\#include "add.h"
\#include "sub.h"
int main() \{ |
| :--- | :--- | :--- |
| | | |
| \#ifndef _SUB_H_ sub.h
\#define _SUB_H_
int sub(int x, int y);
\#endif | \#include "sub.h" sub.c
int sub(int x, int y) \{ return x - y;
\} | printf("5 + 2 - 3 = \%d\n", sub (add (5, 2), 3));
return EXIT_SUCCESS;
\} |
目标是配置构建过程,使每对 .h/.c 文件以及主要的“驱动程序”生成一个独立的 .o 文件。 .o 文件在最后一步中链接在一起。
```
add.c/add.h -> compile -> add.o
sub.c/sub.h -> compile -> sub.o
test.c -> compile -> test.o
add.o sub.o test.o -> link -> a.out
```
(我们也可以更恰当地将 a.out 命名为 test。)

程序定义和引用符号,或在编译单元之间共享的全局变量和函数。
符号定义存储在目标文件中(由汇编器)在符号表中。符号表是结构体的数组。每个条目包括符号的名称、大小和位置。
当一个目标文件使用在另一个编译单元中定义的符号时,它会为该符号留下一个占位符。
链接器的工作是用对该符号的单一定义的引用填充每个符号占位符。
在符号解析步骤中,链接器将每个符号引用与恰好一个符号定义关联起来。
假设这次,您忘记在程序中包含 main 函数(或以某种方式拼写错误)。
您会收到链接器发出的以下消息:
```
/usr/bin/ld:
/usr/lib/gcc/x86_64-linux-gnu/14/../../../x86_
64-linux-gnu/Scrt1.0: in function `_start':
(.text+0x1b): undefined reference to `main'
collect2: error: ld returned 1 exit status
make: *** [Makefile:12: test] Error 1
```
链接器确保定义了 main 符号。
假设您拼写错了 sub 函数,如右图所示。
编译器会就此隐式函数声明向您发出警告,在现代编译器中,警告会转换为错误以防止编译。
```
#include
#include
#include "add.h"
#include "sub.h"
int main() {
printf("5 + 2 - 3 = %d\n",
subb (add (5, 2), 3));
return EXIT_SUCCESS;
}
```
```
test.c: In function 'main':
test.c:7:32: error: implicit declaration of function `subb’;
did you mean 'sub'? [-Wimplicit-function-declaration]
\n",
\textbf{
sub
make: *** [Makefile:16: test.o] Error 1
```
在 C 语言中,如果一个函数在没有事先声明的情况下被调用,编译器会假定一个隐式声明,推断该函数返回一个 int 并接受不定数量的参数。虽然这在旧版本的 C 语言中是允许的,但它被认为是不良实践,并可能导致错误。现代 C 标准(C99 及更高版本)不鼓励隐式声明,编译器通常会发出警告或错误。因此,虽然这以前是链接器的任务,但现在它已被推到编译器来首先捕获。
因此,在 C 语言中,函数在被调用之前必须完全定义或至少声明。函数声明或定义必须存在于调用它的函数的“上方”。
```
// program.c - GOOD
// Declare (but not define) the mult()
// function with a function prototype
int mult(int x, int y);
// Define the "main" symbol
int main(void) {
return mult(3, 4);
}
```
```
// program.c - GOOD
// Define the mult() function above
// the main where it is called.
int mult(int x, int y) {
return x * y;
}
// Define the "main" symbol
int main(void) {
return mult(3, 4);
}
```
```
#include
#include
#include "add.h"
#include "sub.h"
```
我们不是在每个使用函数的源文件顶部手动包含函数原型,而是将原型放入头文件中,并在需要该函数的编译单元开始处 #include 该头文件。
回想一下本幻灯片组前面提到的 add.h。
```
#ifndef _ADD_H_
#define _ADD_H_
int add(int x, int y);
#endif
```
头文件的内容放置在上面红色所示的头文件保护之间。我们将使用的约定是文件名(add.h)的大写字符前后都带有一个下划线。
add.h -> _ADD_H_
头文件保护可防止头文件的内容被多次包含在编译单元中。
头文件保护通常使用预处理器指令(#ifndef、#define、#endif)来检查头文件的内容是否已被包含。第一次包含头文件时,#ifndef 条件为真,头文件的内容被包含。第二次包含时,#define 语句已经设置了宏,因此 #ifndef 条件为假,头文件的内容被跳过。
您应该始终在头文件上包含头文件保护。
不要尝试在 .c 文件上使用头文件保护。
```
foo: foo.c
gcc foo.c -o foo
```
程序员仍然需要正确声明依赖关系!
```
target: dependencies (or prerequisites)
```
```
make <-f makefile> target
```
以操作系统的本机格式生成调试信息。GDB (GNU 调试器) 可以使用此调试信息。
这会启用所有关于某些用户认为有问题的构造的警告,这些构造即使与宏结合使用也很容易避免(或修改以防止警告)。
将所有警告转换为错误。(防止编译)
当基本标准(参见 -Wpedantic)要求诊断时,在某些情况下编译时行为未定义,以及其他一些不会阻止编译根据标准有效程序的情况下,都会给出错误。
总结:在本课程中,将所有三个 -W 标志与 CFLAGS 和 -g 一起使用,以在编译时捕获尽可能多的错误并使调试更容易。
当编译器应该调用链接器 ld 时,提供给编译器的额外标志,例如 -L。库 (-lfoo) 应该添加到 LDLIBS 变量中。
当编译器应该调用链接器 ld 时,提供给编译器的库标志或名称。LOADLIBES 是 LDLIBS 的已弃用(但仍支持)替代方案。非库链接器标志(例如 -L)应该放在 LDFLAGS 变量中。
如果存在名为 all 或 clean 的文件或文件夹,make 将不会执行目标。.PHONY 为我们提供了一种解决此问题的方法。
$(CC) $(CFLAGS) $(LDFLAGS) $(C_FILE) -o $(TARGET) $(LDLIBS)
clean:
rm -f $(TARGET) $(TARGET).exe
限制。
.PHONY: all clean
all:
假设您的源代码在一个名为 hello.c 的文件中。
Makefile 将使用 wildcard 函数查找与 Makefile 位于同一文件夹中的单个 .c 文件。
函数完成后,C_FILE 宏将被赋值为 hello.c。
patsubst 函数将使用 C_FILE 的值并匹配所有字符直到 .c 扩展名。在这种情况下,匹配结果为 hello,它被赋值给 TARGET。
-o $@
将编译的输出放在 : 左侧命名的文件中,即该规则的目标。
$<
依赖项列表中的第一个项目。
-c 标志表示只编译;不链接。
我们使用 -c 用于需要我们生成目标文件而不是可执行文件的规则。
```
CC = gcc
TARGET =
C_FILES = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(C_FILES))
CFLAGS = -g -Wall -Werror -pedantic-errors
LDFLAGS =
LDLIBS =
```
```
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$ (CC) $(LDFLAGS) $(OBJS) -O $@ $(LDLIBS)
%.O: %.C %.h
$ (CC) $(CFLAGS) -C -O $@ $<
%.O: %.C
$ (CC) $(CFLAGS) -C -O $@ $<
clean:
rm -f $(OBJS) $(TARGET) $(TARGET).exe
```
make 会打印它运行的命令。
假设我们按如下方式构建项目:
```
$ make
gcc -g -Wall -Werror -pedantic-errors -c -o add.o add.c
gcc -g -Wall -Werror -pedantic-errors -c -o sub.o sub.c
gcc -g -Wall -Werror -pedantic-errors -c -o test.o test.c
gcc add.o sub.o test.o -o test
If we run make again, nothing happens, since the executable test has a later timestamp than its dependencies.
```
```
$ make
make: Nothing to be done for 'all'.
```
我们可以使用 touch 命令更新文件的最后修改日期。
```
$ touch add.h
$ make
gcc -g -Wall -Werror
-pedantic-errors -c -o add.o add.c
gcc add.o sub.o test.o -o test
```
请注意,add.h 是 add.o 的先决条件。因此,add.o 被重建,然后所有 .o 文件被重新链接以构建 test 可执行文件。
同样,我们可以使用 touch 命令更新文件的最后修改日期。
$ touch add.o
$ make
gcc add.o sub.o test.o -o test
add.o 是 test 可执行文件的先决条件。
没有发生编译。
只需重新链接即可生成更新的 test 可执行文件。大学
```
$ gcc -dM -E -x c /dev/null | grep -F __STDC_VERSION__
#define __STDC_VERSION__ 201710L
```
您可能需要在某个时候调试 Makefile。您可以使用 info 函数打印变量的值。请注意,连续两个 $` 会打印一个 `$ 并阻止引用变量。
```
$(TARGET): $(OBJS)
$(info $$(OBJS) is $(OBJS))
$(CC) $(OBJS) -O $(TARGET) $(LDFLAGS)
```
COMS W3157
Dr. Borowski
https://www.kernel.org/doc/Documentation/process/coding-styl e.rst
https://en.wikipedia.org/wiki/Indentation style
| auto | break | case | char | const | continue | default | do |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| double | else | enum | extern | float | for | goto | if |
| int | long | register | return | short | signed | sizeof | static |
| struct | switch | typedef | union | unsigned | void | volatile | while |
有关更多信息,请参阅 https://www.geeksforgeeks.org/keywords-in-c/。
| 优先级 | | 运算符 | 描述 | 结合性 |
| :--- | :--- | :--- | :--- | :--- |
| | 1 | ++ --
()
[]
.
->
(type){list} | 后缀/后置自增和自减
函数调用
数组下标
结构体和联合体成员访问
通过指针的结构体和联合体成员访问
复合字面量(C99) | 左结合 |
| | 2 | ++ --
+ -
! ~
(type)
*
&
sizeof
Alignof | 前缀自增和自减 ${ }^{[\text {note 1] }}$
一元加和减
逻辑非和位非
类型转换
间接寻址(解引用)
取地址
大小 ${ }^{[\text {note 2] }}$
对齐要求(C11) | 右结合 |
| 3 | * / % | 乘法、除法和取余 | 左结合 |
| :--- | :--- | :--- | :--- |
| 4 | + - | 加法和减法 | |
| 5 | << >> | 位左移和右移 | |
| 6 | <=
>= | 分别用于关系运算符 < 和 $\leq$
分别用于关系运算符 > 和 $\geq$ | |
| 7 | == != | 分别用于关系运算符 = 和 $\neq$ | |
| 8 | & | 位与 | |
| 9 | ^ | 位异或(异或) | |
| 10 | | | 位或(包含或) | |
| 11 | && | 逻辑与 | |
| 12 | || | 逻辑或 | |
| 13 | ?: | 三元条件 ${ }^{[\text {note 3] }}$ | 右结合 |
| :--- | :--- | :--- | :--- |
| $14^{[\text {note 4] }}$ | =
+= -=
*= /= %=
<<= >> =
&= ^= |= | 简单赋值
加减赋值
乘除取余赋值
位左移右移赋值
位与异或或赋值 | |
| 15 | , | 逗号 | 左结合 |
有关更多详细信息,请参阅 https://en.cppreference.com/w/c/language/operator precedence.html。
在 C 语言中,类型转换是一种非常有用的工具,可以将值从一种数据类型更改为另一种数据类型。
为什么我们需要类型转换?
也许我们想要为某个值分配更多内存(例如:int -> double),或者在字母和数字之间切换(例如:char -> int),等等。
类型转换有两种不同的方法:
两种方法的示例将在后面介绍!



```
int main(int argc, char **argv) {
char var1 = 0x41; // type cast: 0x41 -> 'A'; integer-> char
int var2 = 1.5; // type cast: 1.5 -> 1; float -> integer
// type cast: -1 -> Oxffffffff; signed -> unsigned
unsigned int var3 = -1;
}
```
char c = 0x41424344; c 的值是多少?假设未启用 -Werror=overflow。
```
int main(int argc, char **argv) {
char var1 = 0x41; // type cast: Ox41 -> 'A'; integer-> char
int var2 = 1.5; // type cast: 1.5 -> 1; float -> integer
// type cast: -1 -> Oxffffffff; signed -> unsigned
unsigned int var3 = -1;
}
```
char c = 0x41424344;
c 的值是多少? $16^{1} \times 4+16^{0} \times 4=68$,或 'D' 假设未启用 -Werror=overflow。
为了保证 C 语言中的类型转换为新类型,请使用类型转换运算符,其语法为 (new_type) value。
```
int main(int argc, char **argv) {
float f;
int a = 20, b = 3;
f = a/b; // What is f? 6
f = (float)a/b; // What is f now? ~6.666667
```
}
宏通常用于定义永远不会改变并且程序其他部分经常访问的值。
使用 #define
预处理器在将源代码传递给编译器之前,将所有 PI 实例替换为 3.14。
```
#define PI 3.14
int main(int argc, char **argv) {
float pi = PI; // pi = 3.14
return 0;
}
```
```
enum week { Sunday, Monday, Tuesday,
Wednesday, Thursday, Friday, Saturday };
int main(int argc, char **argv) {
enum week today;
today = Wednesday; // today = ?
return 0;
}
```
```
enum week { Sunday, Monday, Tuesday,
Wednesday, Thursday, Friday, Saturday };
int main(int argc, char **argv) {
enum week today;
today = Wednesday; // today = 3
return 0;
}
```
结构体是变量的集合(可以是不同类型),属于单一类型。
```
struct my_struct {
int x;
int y;
int z;
};
```
```
struct book {
int year;
int month;
int book_id;
}; // Do not forget the ";" here.
int main() {
struct book book1; // declare book1 of type Books
book1.book_id = 100; // access member in struct
return 0;
}
```
联合体用于在相同的内存位置存储不同类型的数据。
```
union my_union {
int i;
short s;
char c;
};
```
联合体需要足够的空间来存储其最大的成员。然而,它只能容纳一个信息;对一个成员的赋值会影响其他成员。
my_union 的大小是多少?
```
union my_union { int i; short s; char c; };
int main(int argc, char **argv) {
union my_union test;
int var0;
test.i = 0;
test.c = 'A';
test.s = 16383;
varO = test.c;
}
```
```
Examples of arrays allocated on the stack:
int a[10]; // type name[size1]
char b[10][100];
struct my_struct c[5][10][20];
```
```
int main(int argc, char **argv) {
int array[10];
array[1] = 100;
array[2] = 200;
array[3] = array[1] + array[2];
array[0] = 1000; // Is this right?
array[10] = 1000; // Is this right?
}
```
```
int main(int argc, char **argv) {
int array[10];
array[1] = 100;
array[2] = 200;
array[3] = array[1] + array[2];
array[0] = 1000; // Yes, arrays are 0-based.
array[10] = 1000; // No, valid indices are 0-9.
}
```
数组可以按如下方式显式初始化:
```
int a[3];
a[O] = O; a[1] = 1; a[2] = 2;
// 0, 1, 2 assigned to a[0], a[1], a[2], respectively.
int a[3] = { 0, 1, 2 };
// Same as int a[3] = { 0, 1, 2 }; Size inferred.
int a[] = { 0, 1, 2 };
// Same as int a[3] = { 1, 0, 0 };
int a[3] = { 1 };
// Initializes all 100 elements to 0.
// The first element is explicitly 0, the rest implicitly.
int a[100] = { 0 };
```
它是一个看起来像函数的运算符。返回类型或变量的大小(以字节为单位)。
```
int x = 56;
// both sizeof(x) and sizeof(int) return 4
int a[3];
// sizeof(a) returns 12 (3 times sizeof(int))
```
(C 语言中最重要的概念之一)
内存就像一个由字节组成的长数组。
| 72 | 101 | 108 | 108 | 111 | 32 | 65 | 80 | 33 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
指针存储字节的索引(或字节块的起始地址),称为内存地址。
例如,值为 5 的指针将指向上述示例中的值 32。(细节有点复杂,但这是基本思想。)大学
| 72 | 101 | 108 | 108 | 111 | 32 | 65 | 80 | 33 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
如果我们有一个名为 p 的指针,它指向地址 5,我们可以:
理解指针的关键是理解指针本身存储在内存中的某个位置,所以我们可以有一个指向指针的指针!

地址 3 指向地址 6,地址 6 指向地址 1。
int intptr; // 指向一个整型 char charptr; // 指向一个字符型 struct my_struct *structptr; // 指向一个结构体 int intptrptr; // 指向一个整型指针**
| | 8 字节
" | | 4 字节 | 4 字节 | | 4 字节 | |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| | | | | | | |
|
| int *p; int a,b,c; a = 1; | □
uninitialized | | 1 | uninitialized | | uninitialized | |
| | p | 999 | 1000 | 1004 | b | 1008 | C |

由于我们有一个指向 a 的指针,我们可以使用解引用运算符 (*) 访问和修改 a 的值。
这两行具有相同的效果。

*p 等同于说:“跟随粉色箭头并访问那里的值。”
哥伦比亚

C 语言中指针的一些更细致的细节:
C 语言中指针的一些更细致的细节:
```
int *p;
double *x = p; // compiler error: incompatible pointer types
Instead we should write double x = (double ) p.
```
```
void *p;
int x = p; // compiler error: cannot dereference void
```
C 语言中指针的一些更细致的细节:
```
int *p = NULL;
int x = *p; // segmentation fault at runtime
```
```
typedef
typedef unsigned int uint; typedef struct my_struct {
...
} my_struct;
typedef union my_union {
int i;
char c;
double d;
char data[sizeof(double)];
} my_union;
```
类型定义缩短了整个项目中的声明!
```
typedef struct book {
int year;
int month;
int book_id;
} book; // do not forget about the ";" here
int main() {
book book1; // struct Book Book1;
book1.book_id = 100;
return 0;
}
```
变量作用域有三种:局部、全局、静态。这些不同作用域的级别告诉我们从哪里可以访问变量。
```
void func1() {
int i;
i = 1;
}
void func2() {
i = 1; // invalid scope (we can't access 'i' from
// outside func1 without redeclaring it)
}
```
```
int g_debug_level;
void func1() {
g_debug_level = 1;
}
void func2() {
g_debug_level = 2;
}
void func3() {
g_debug_level = 3;
}
```
```
static int g_debug_level; // in file1.c
void func1() { // in file1.c
g_debug_level = 1; // OK
}
void func2() { // in file2.c
g_debug_level = 2; // NOT OK
}
```
```
int static_integer() {
static int i = 100;
i++;
return i;
}
int main() {
for (int i = 0; i < 5; i++) {
printf("%d\n", static_integer());
}
}
```
声明函数很容易!
```
int my_func(char arg1, int arg2, float arg3);
// return_type, function_name(arguments list)
```
函数可见性:
```
static int my_func(char arg1, int arg2, float arg3);
```
C 语言只使用传值调用!
这意味着子函数所做的更改不会影响父函数。
```
int main() {
int i = 0;
callee(i);
printf("%d\n", i);
return 0;
}
```
```
void callee(int c) {
c = 10;
}
```
此程序的输出是什么?
C 语言只使用传值调用!
这意味着子函数所做的更改不会影响父函数。
```
int main() {
int i = 0;
callee(i);
printf("%d\n", i);
return 0;
}
```
```
void callee(int c) {
c = 10;
}
```
此程序的输出是什么? 0
我们还可以使用函数的返回值更新变量的值。
```
int main() {
int i = 0;
i = callee(i);
printf("%d\n", i);
return 0;
}
```
```
int callee(int c) {
c = 10;
return c;
}
```
此程序的输出是什么?
我们还可以使用函数的返回值更新变量的值。
```
int main() {
int i = 0;
i = callee(i);
printf("%d\n", i);
return 0;
}
```
```
int callee(int c) {
c = 10;
return c;
}
```
此程序的输出是什么? 10
我们还可以使用指针更新变量的值。
```
int main() {
int i = 0;
callee(&i);
printf("%d\n", i);
return 0;
}
```
```
void callee(int *c) {
*c = 10; // dereferencing
}
```
此程序的输出是什么?
我们还可以使用指针更新变量的值。
```
int main() {
int i = 0;
callee(&i);
printf("%d\n", i);
return 0;
}
```
```
void callee(int *c) {
*c = 10; // dereferencing
}
```
此程序的输出是什么? 10
正如我们可以有一个指向内存中其他位置数据的指针变量一样,我们也可以有一个存储函数地址的函数指针,从而允许函数作为参数传递并动态调用。
在本课程中,我们最常使用函数指针来实现多态性。
```
#include
int add(int a, int b) { return a + b; }
void say_hi() { puts("Hi"); }
int main() {
// Declare a function pointer that matches the signature of add() function.
int (*add_ptr) (int, int);
// Declare a function pointer that matches the signature of say_hi() function.
void (*say_hi_ptr) ();
// Assign function pointers.
add_ptr = &add; say_hi_ptr = &say_hi;
// Call the functions via their function pointers.
printf("%d", add_ptr(2, 3));
say_hi_ptr();
return 0;
}
```
```
/**
*/
int int_cmp(int a, int b) {
if (a > b) {
return 1;
}
if (a < b) {
return -1;
}
return 0;
}
int binary_search(const int key, const int *values,
const size_t num_elems, int (*cmp) (int, int)) {
...
int result = cmp(key, values[mid]);
}
int array[] = { 1, 4, 7, 18, 90 };
int retval = binary_search(4, array, 5, int_cmp);
```